﻿/****************************************************************************************************
	Copyright (C) 2010 RapidWebDev Organization (http://rapidwebdev.org)
	Author: Eunge, Legal Name: Jian Liu, Email: eunge.liu@RapidWebDev.org

	This program is free software: you can redistribute it and/or modify
	it under the terms of the GNU General Public License as published by
	the Free Software Foundation, either version 2 of the License, or
	(at your option) any later version.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program.  If not, see <http://www.gnu.org/licenses/>.
 ****************************************************************************************************/

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Configuration;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Web.UI;
using System.Xml;
using System.Xml.Schema;
using RapidWebDev.Common;
using RapidWebDev.Common.Data;
using RapidWebDev.ExtensionModel;
using RapidWebDev.FileManagement;
using RapidWebDev.Platform;
using RapidWebDev.SearchIndex.Contract;
using RapidWebDev.SearchIndex.Parser.IndexEntityMapping;
using System.Threading;

namespace RapidWebDev.SearchIndex.Parser
{
	/// <summary>
	/// Api to index entities configured in mapping xml file.
	/// </summary>
	public class EntityIndexApi : IEntityIndexApi
	{
		/// <summary>
		/// The prefix is "Ext_"
		/// </summary>
		public const string ExtensionPropertyPrefix = "Ext_";

		/// <summary>
		/// The property is "Ext_ExtensionDataTypeId"
		/// </summary>
		public const string ExtensionDataTypeIdProperty = "Ext_ExtensionDataTypeId";

		private static readonly object syncObject = new object();
		private string indexMappingFilePath;
		private string fileSharedPath;
		private IDictionary<string, Assembly> assemblies;
		private Dictionary<int, List<IndexingEntityQueueItem>> pendingIndexingEntitiesByThread = new Dictionary<int, List<IndexingEntityQueueItem>>();
		private IApplicationApi applicationApi;
		private IApplicationContext applicationContext;
		private IMetadataApi metadataApi;
		private IFileBindingApi fileBindingApi;
		private IIndexApi indexApi;

		private IDictionary<string, Assembly> Assemblies
		{
			get
			{
				if (this.assemblies == null)
				{
					lock (syncObject)
					{
						if (this.assemblies == null)
						{
							this.assemblies = ResolveConfiguration();
							TransactionScopeContext.AfterNewTransactionStarted += new Action<int>(ResetIndexingQueue);
							TransactionScopeContext.AfterTransactionCommitted += new Action<int>(CommitIndexingQueue);
							TransactionScopeContext.AfterTransactionRollback += new Action<int>(ResetIndexingQueue);
						}
					}
				}

				return this.assemblies;
			}
		}

		/// <summary>
		/// Get xml parser instance.
		/// </summary>
		private static XmlParser @XmlParser
		{
			get
			{
				XmlNamespaceManager xmlNamespaceManager = new XmlNamespaceManager(new NameTable());
				xmlNamespaceManager.AddNamespace("p", "http://www.rapidwebdev.org/schemas/indexentitymapping");

				string schemaXml = Kit.GetManifestFile(typeof(EntityIndexApi), "IndexEntityMapping.xsd");
				using (StringReader sr = new StringReader(schemaXml))
				{
					return new XmlParser(xmlNamespaceManager, XmlSchema.Read(sr, null));
				}
			}
		}

		/// <summary>
		/// Constructor
		/// </summary>
		/// <param name="applicationContext"></param>
		/// <param name="applicationApi"></param>
		/// <param name="metadataApi"></param>
		/// <param name="fileBindingApi"></param>
		/// <param name="indexApi"></param>
		/// <param name="indexMappingFilePath"></param>
		/// <param name="fileSharedPath"></param>
		public EntityIndexApi(IApplicationContext applicationContext, IApplicationApi applicationApi, IMetadataApi metadataApi, IFileBindingApi fileBindingApi, IIndexApi indexApi, string indexMappingFilePath, string fileSharedPath)
		{
			Kit.NotNull(applicationContext, "applicationContext");
			Kit.NotNull(applicationApi, "applicationApi");
			Kit.NotNull(metadataApi, "metadataApi");
			Kit.NotNull(fileBindingApi, "fileBindingApi");
			Kit.NotNull(indexApi, "indexApi");
			Kit.NotNull(indexMappingFilePath, "indexMappingFilePath");
			Kit.NotNull(fileSharedPath, "fileSharedPath");

			this.applicationContext = applicationContext;
			this.applicationApi = applicationApi;
			this.metadataApi = metadataApi;
			this.fileBindingApi = fileBindingApi;
			this.indexApi = indexApi;

			this.indexMappingFilePath = Kit.ToAbsolutePath(indexMappingFilePath);
			if (!File.Exists(this.indexMappingFilePath))
				throw new ConfigurationErrorsException("The file is not found by the parameter indexMappingFilePath of EntityIndexApi.");

			this.fileSharedPath = fileSharedPath;
			if (string.IsNullOrEmpty(this.fileSharedPath))
				throw new ConfigurationErrorsException(string.Format(CultureInfo.InvariantCulture, "The file shared path \"{0}\" is null or empty by the parameter fileSharedPath of EntityIndexApi.", this.fileSharedPath));
		}

		/// <summary>
		/// Save an entity into index service.
		/// </summary>
		/// <param name="entity"></param>
		public void Save(object entity)
		{
			Entity entityMapping = ResolveEntityMapping(entity);
			if (entityMapping == null) return;
			IndexingEntityQueueItem queueItem = new IndexingEntityQueueItem(IndexingEntityQueueItem.CommandTypes.Save, entity, entityMapping);

			if (TransactionScopeContext.HasTransaction)
			{
				int managedThreadId = System.Threading.Thread.CurrentThread.ManagedThreadId;
				if (!this.pendingIndexingEntitiesByThread.ContainsKey(managedThreadId))
					this.pendingIndexingEntitiesByThread[managedThreadId] = new List<IndexingEntityQueueItem>();

				this.pendingIndexingEntitiesByThread[managedThreadId].Add(queueItem);
			}
			else
				this.SubmitIndexQueueItems(new[] { queueItem });
		}

		/// <summary>
		/// Delete an entity from index service.
		/// </summary>
		/// <param name="entity"></param>
		public void Delete(object entity)
		{
			Entity entityMapping = ResolveEntityMapping(entity);
			if (entityMapping == null) return;
			IndexingEntityQueueItem queueItem = new IndexingEntityQueueItem(IndexingEntityQueueItem.CommandTypes.Delete, entity, entityMapping);

			if (TransactionScopeContext.HasTransaction)
			{
				int managedThreadId = System.Threading.Thread.CurrentThread.ManagedThreadId;
				if (!this.pendingIndexingEntitiesByThread.ContainsKey(managedThreadId))
					this.pendingIndexingEntitiesByThread[managedThreadId] = new List<IndexingEntityQueueItem>();

				this.pendingIndexingEntitiesByThread[managedThreadId].Add(queueItem);
			}
			else
				this.SubmitIndexQueueItems(new[] { queueItem });
		}

		private IDictionary<string, Assembly> ResolveConfiguration()
		{
			XmlDocument xmldoc = new XmlDocument();
			xmldoc.Load(this.indexMappingFilePath);

			XmlParser xmlParser = @XmlParser;
			try
			{
				Kit.ValidateXml(xmlParser.Schema, xmldoc);
			}
			catch (Exception exp)
			{
				throw new ConfigurationErrorsException("The index mapping file is invalid schema which is configured in the attribute MappingFilePath of the configuration section \"indexConfig\" in application configuration.", exp);
			}

			Dictionary<string, Assembly> assemblyResults = new Dictionary<string, Assembly>();
			XmlNodeList assemblyNodeList = xmldoc.SelectNodes("//p:Assembly", xmlParser.NamespaceManager);
			foreach (XmlElement assemblyElement in assemblyNodeList)
			{
				Assembly assembly = new Assembly(assemblyElement, xmlParser);
				assemblyResults[assembly.Name] = assembly;
			}

			return assemblyResults;
		}

		private Entity ResolveEntityMapping(object entity)
		{
			if (entity == null) return null;

			Type entityType = entity.GetType();
			string assemblyName = entityType.Assembly.GetName().Name;
			if (!Assemblies.ContainsKey(assemblyName)) return null;

			Assembly assemblyMapping = Assemblies[assemblyName];
			string clrType = entityType.Namespace + "." + entityType.Name;
			if (!assemblyMapping.Entities.ContainsKey(clrType)) return null;

			return assemblyMapping.Entities[clrType];
		}

		private void ResetIndexingQueue(int threadId)
		{
			if (!this.pendingIndexingEntitiesByThread.ContainsKey(threadId)) return;
			this.pendingIndexingEntitiesByThread[threadId].Clear();
		}

		private void CommitIndexingQueue(int threadId)
		{
			if (!this.pendingIndexingEntitiesByThread.ContainsKey(threadId)) return;
			IEnumerable<IndexingEntityQueueItem> queueItems = this.pendingIndexingEntitiesByThread[threadId];
			this.SubmitIndexQueueItems(queueItems);
			this.pendingIndexingEntitiesByThread[threadId].Clear();
		}

		private void SubmitIndexQueueItems(IEnumerable<IndexingEntityQueueItem> queueItems)
		{
			ApplicationObject applicationObject = this.applicationApi.Get(this.applicationContext.ApplicationId);
			string stemmingLanguage = (applicationObject["StemmingLanguage"] as string) ?? "English";

			string applicationId = this.applicationContext.ApplicationId.ToString();
			Collection<IndexDocMessage> indexDocsToSave = new Collection<IndexDocMessage>();
			Collection<IndexDocMessage> indexDocsToDelete = new Collection<IndexDocMessage>();
			IndexingEntityQueueItem.CommandTypes? previousCommandType = null;
			foreach (IndexingEntityQueueItem queueItem in queueItems)
			{
				IndexDocMessage indexDoc = new IndexDocMessage
				{
					ApplicationId = applicationId,
					StemmingLanguage = stemmingLanguage,
					Type = queueItem.Mapping.DocType,
					Command = queueItem.Command.ToString()
				};

				ParseEntityId(indexDoc, queueItem);
				ParseEntityProperties(indexDoc, queueItem);
				ParseEntityAttachment(indexDoc, queueItem);
				ParseEntityExtensionProperties(indexDoc, queueItem);

				if (previousCommandType.HasValue)
				{
					if (queueItem.Command == IndexingEntityQueueItem.CommandTypes.Save && queueItem.Command != previousCommandType.Value)
					{
						this.indexApi.DeleteDocuments(indexDocsToDelete);
						indexDocsToDelete.Clear();
					}
					else if (queueItem.Command == IndexingEntityQueueItem.CommandTypes.Delete && queueItem.Command != previousCommandType.Value)
					{
						this.indexApi.SubmitMessages(indexDocsToSave);
						indexDocsToSave.Clear();
					}
				}

				previousCommandType = queueItem.Command;
				if (queueItem.Command == IndexingEntityQueueItem.CommandTypes.Save)
					indexDocsToSave.Add(indexDoc);
				else if (queueItem.Command == IndexingEntityQueueItem.CommandTypes.Delete)
					indexDocsToDelete.Add(indexDoc);
			}

			if (indexDocsToSave.Count > 0)
				this.indexApi.SubmitMessages(indexDocsToSave);
			if (indexDocsToDelete.Count > 0)
				this.indexApi.DeleteDocuments(indexDocsToDelete);
		}

		#region Parse entity to IndexDocMessage

		private void ParseEntityId(IndexDocMessage indexDoc, IndexingEntityQueueItem queueItem)
		{
			object idPropertyValue = DataBinder.Eval(queueItem.Entity, queueItem.Mapping.IdPropertyName);
			if (idPropertyValue == null)
				throw new ConfigurationErrorsException(string.Format(CultureInfo.InvariantCulture, "The Id property value \"{0}\" not found in the entity \"{1}\".", queueItem.Mapping.IdPropertyName, queueItem.Entity.ToString()));

			indexDoc.DocumentId = idPropertyValue.ToString();
		}

		private void ParseEntityProperties(IndexDocMessage indexDoc, IndexingEntityQueueItem queueItem)
		{
			if (queueItem.Command != IndexingEntityQueueItem.CommandTypes.Save) return;

			indexDoc.Fields = new Collection<IndexDocFieldMessage>();
			foreach (Property propertyMapping in queueItem.Mapping.Properties)
			{
				object propertyValue = DataBinder.Eval(queueItem.Entity, propertyMapping.PropertyName);
				if (propertyValue == null) continue;

				IndexDocFieldMessage indexDocField = new IndexDocFieldMessage
				{
					Name = propertyMapping.FieldName,
					Type = propertyMapping.FieldType,
					Values = ConvertEntityPropertyValue(propertyValue),
					Sortable = propertyMapping.Sortable
				};

				indexDoc.Fields.Add(indexDocField);
			}
		}

		private void ParseEntityAttachment(IndexDocMessage indexDoc, IndexingEntityQueueItem queueItem)
		{
			if (queueItem.Command != IndexingEntityQueueItem.CommandTypes.Save) return;

			indexDoc.Attachments = new Collection<IndexDocAttachmentMessage>();
			foreach (Attachment attachmentMapping in queueItem.Mapping.Attachments)
			{
				try
				{
					Guid externalObjectId = (Guid)DataBinder.Eval(queueItem.Entity, attachmentMapping.PropertyName);
					IEnumerable<FileHeadObject> fileHeadObjects = this.fileBindingApi.FindBoundFiles(externalObjectId, attachmentMapping.RelationshipType);
					foreach (FileHeadObject fileHeadObject in fileHeadObjects)
					{
						IndexDocAttachmentMessage indexDocAttachment = new IndexDocAttachmentMessage
						{
							Id = fileHeadObject.Id.ToString(),
							Name = fileHeadObject.FileName,
							SharedPath = Path.Combine(fileSharedPath, ResolveFileStoragePath(this.applicationContext.ApplicationId, fileHeadObject))
						};

						indexDoc.Attachments.Add(indexDocAttachment);
					}
				}
				catch
				{
					throw new ConfigurationErrorsException(string.Format(CultureInfo.InvariantCulture, "The property \"{0}\" of attachment is not found in the entity \"{1}\" or is not a Guid value.", attachmentMapping.PropertyName, queueItem.Entity.ToString()));
				}
			}
		}

		private void ParseEntityExtensionProperties(IndexDocMessage indexDoc, IndexingEntityQueueItem queueItem)
		{
			if (queueItem.Command != IndexingEntityQueueItem.CommandTypes.Save) return;
			if (!queueItem.Mapping.ParseExtensionProperties) return;

			IExtensionObject extensionObject = queueItem.Entity as IExtensionObject;
			if (extensionObject == null) return;

			IEnumerable<IFieldMetadata> fieldMetadataEnumerable = this.metadataApi.GetFields(extensionObject.ExtensionDataTypeId);

			// store extension data type id.
			IndexDocFieldMessage indexDocField = new IndexDocFieldMessage
			{
				Name = ExtensionDataTypeIdProperty,
				Type = IndexDocFieldValueTypes.Keyword,
				Values = new Collection<string> { extensionObject.ExtensionDataTypeId.ToString() }
			};
			indexDoc.Fields.Add(indexDocField);

			foreach (IFieldMetadata fieldMetadata in fieldMetadataEnumerable)
			{
				object propertyValue = extensionObject[fieldMetadata.Name];
				if (propertyValue == null) continue;

				indexDocField = new IndexDocFieldMessage { Name = ExtensionPropertyPrefix + fieldMetadata.Name };
				indexDoc.Fields.Add(indexDocField);
				switch (fieldMetadata.Type)
				{
					case FieldType.Date:
						indexDocField.Type = IndexDocFieldValueTypes.Date;
						indexDocField.Values = new Collection<string> { propertyValue.ToString() };
						break;
					case FieldType.DateTime:
						indexDocField.Type = IndexDocFieldValueTypes.DateTime;
						indexDocField.Values = new Collection<string> { propertyValue.ToString() };
						break;
					case FieldType.Decimal:
						indexDocField.Type = IndexDocFieldValueTypes.Decimal;
						indexDocField.Values = new Collection<string> { propertyValue.ToString() };
						break;
					case FieldType.Integer:
						indexDocField.Type = IndexDocFieldValueTypes.Integer;
						indexDocField.Values = new Collection<string> { propertyValue.ToString() };
						break;
					case FieldType.String:
						indexDocField.Type = IndexDocFieldValueTypes.Text;
						indexDocField.Values = new Collection<string> { propertyValue.ToString() };
						break;
					case FieldType.Enumeration:
						indexDocField.Type = IndexDocFieldValueTypes.Keyword;
						EnumerationValueCollection enumerationValues = propertyValue as EnumerationValueCollection;
						if (enumerationValues == null) continue;
						indexDocField.Values = enumerationValues;
						break;
				}
			}
		}

		private static Collection<string> ConvertEntityPropertyValue(object propertyValue)
		{
			if (propertyValue is string)
				return new Collection<string> { propertyValue as string };

			if (propertyValue is IEnumerable)
			{
				Collection<string> values = new Collection<string>();
				IEnumerator enumerator = (propertyValue as IEnumerable).GetEnumerator();
				while (enumerator.MoveNext())
					if (enumerator.Current != null)
						values.Add(enumerator.Current.ToString());

				return values;
			}

			return new Collection<string> { propertyValue.ToString() };
		}

		private static string ResolveFileStoragePath(Guid applicationId, FileHeadObject fileHeadObject)
		{
			string categoryFolderName = ResolveValidFolderName(fileHeadObject.Category);
			string directoryPath = Path.Combine(applicationId.ToString(), categoryFolderName);
			return Path.Combine(directoryPath, string.Format(CultureInfo.InvariantCulture, "{0}.{1}", fileHeadObject.Id, fileHeadObject.FileExtensionName));
		}

		private static string ResolveValidFolderName(string category)
		{
			if (string.IsNullOrEmpty(category)) return "";

			string output = "";
			foreach (char ch in category)
				if (!Path.GetInvalidFileNameChars().Contains(ch))
					output += ch.ToString();

			return output;
		}

		#endregion
	}
}